import requests
from bs4 import BeautifulSoup
import json
from tqdm import tqdm
import time
import functools
import logging
import sys

# --- 配置日志记录器 ---
# 创建一个日志记录器
logger = logging.getLogger('NovelScraper')
logger.setLevel(logging.DEBUG)

# 创建一个处理器，用于将日志输出到控制台
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO) # 控制台只显示 INFO 及以上级别
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)

# 创建一个处理器，用于将日志写入文件
file_handler = logging.FileHandler('novel_scraper.log', encoding='utf-8')
file_handler.setLevel(logging.DEBUG) # 文件中记录所有 DEBUG 及以上级别
file_handler.setFormatter(formatter)

# 将处理器添加到日志记录器
logger.addHandler(console_handler)
logger.addHandler(file_handler)

def retry(max_attempts=3, delay=1):
    """
    一个用于重试函数的装饰器，用于处理网络请求失败。
    :param max_attempts: 最大重试次数。
    :param delay: 每次重试前的延迟时间（秒）。
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    result = func(*args, **kwargs)
                    if result is not None:
                        return result
                    else:
                        logger.warning(f"尝试 {attempt} 获取 {func.__name__} 失败：返回 None。正在重试...")
                except requests.exceptions.RequestException as e:
                    logger.warning(f"尝试 {attempt} 获取 {func.__name__} 失败：{e}")
                if attempt < max_attempts:
                    logger.info(f"正在等待 {delay} 秒后重试...")
                    time.sleep(delay)
            logger.error(f"经过 {max_attempts} 次尝试后，{func.__name__} 最终失败。")
            return None
        return wrapper
    return decorator

class NovelScraper:
    """
    一个用于爬取小说信息和内容的类。
    """
    def __init__(self, base_url, headers=None, timesleep=2):
        """
        初始化小说爬取器。
        :param base_url: 小说网站的基础 URL，用于拼接章节链接。
        :param headers: 请求头，默认为常用浏览器 User-Agent。
        :param timesleep: 每次请求后的延迟时间（秒），以避免过快访问。
        """
        self.base_url = base_url.rstrip('/') + '/'
        self.headers = headers if headers else {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
        }
        self.timesleep = timesleep
        self.novel_data = {
            "author": None,
            "desc": None,
            "index_href": None,
            "content": []
        }
        self.chapters_parsed_count = 0
        self.logger = logging.getLogger('NovelScraper')

    @retry(max_attempts=5, delay=1)
    def _fetch_html_content(self, url):
        """
        私有方法：获取指定 URL 的 HTML 内容。
        """
        self.logger.debug(f"正在尝试获取 URL: {url}")
        try:
            response = requests.get(url, headers=self.headers, timeout=15)
            response.raise_for_status()
            time.sleep(self.timesleep)
            self.logger.debug(f"成功获取 URL: {url}")
            return response.text
        except requests.exceptions.HTTPError as e:
            self.logger.error(f"HTTP错误: {url} 返回状态码 {e.response.status_code}")
            return None
        except requests.exceptions.RequestException as e:
            self.logger.error(f"请求失败: {url} - {e}")
            return None

    def _parse_chapter_list(self, html_content):
        """
        私有方法：从 HTML 内容中解析章节名称和链接。
        """
        soup = BeautifulSoup(html_content, 'html.parser')
        chap_dict = {}
        try:
            chapter_dl = soup.find_all('dl')[1]
            for tag in chapter_dl.find_all('dd'):
                if tag.a and '展开全部章节' not in tag.a.string:
                    chap_name = tag.a.string.strip()
                    href = self.base_url + tag.a['href'].lstrip('/')
                    chap_dict[chap_name] = href
        except IndexError:
            self.logger.warning("未找到预期的章节列表结构。请检查网站 HTML 结构是否已更改。")
        except AttributeError:
            self.logger.warning("在解析章节列表时出现属性错误，可能缺少 a 标签。")
        return chap_dict

    def _extract_novel_metadata(self, html_content, index_href):
        """
        私有方法：从 HTML 内容中提取小说元数据（作者、描述）。
        """
        soup = BeautifulSoup(html_content, 'html.parser')
        author, desc = None, None
        try:
            author_tag = soup.find("div", {"class": 'small'})
            if author_tag:
                author_span = author_tag.find('span')
                author = author_span.string
            
            desc_tag = soup.find("div", {"class": "intro"})
            if desc_tag:
                desc = desc_tag.find('dd').get_text(strip=True)
        except (AttributeError, IndexError):
            self.logger.warning("未找到预期的元数据元素。")

        self.novel_data["author"] = author
        self.novel_data["desc"] = desc
        self.novel_data["index_href"] = index_href
        self.logger.info(f"成功提取小说元数据：作者={author}, 描述={desc[:30]}...")


    def _parse_chapter_content(self, html_content):
        """
        私有方法：从章节 HTML 内容中提取正文。
        """
        soup = BeautifulSoup(html_content, 'html.parser')
        content = ""
        try:
            content_div = soup.find(id='chaptercontent')
            if content_div:
                content = content_div.get_text(strip=True, separator='\n')
        except AttributeError:
            self.logger.warning("未找到章节内容 div。")
        return content

    def scrape_novel(self, chap_url, max_chapters=20):
        """
        执行小说爬取的主要方法。
        :param chap_url: 章节目录页的 URL。
        :param max_chapters: 最大爬取章节数，默认为 20。
        """
        self.logger.info(f"正在从 {chap_url} 获取小说目录页...")
        main_page_html = self._fetch_html_content(chap_url)
        if not main_page_html:
            self.logger.error("获取主目录页失败。程序退出。")
            return False

        self._extract_novel_metadata(main_page_html, chap_url)
        chap_dict = self._parse_chapter_list(main_page_html)

        if not chap_dict:
            self.logger.error("未找到任何章节。程序退出。")
            return False
        
        self.logger.info(f"共找到 {len(chap_dict)} 个章节，开始下载最多 {max_chapters} 个章节...")
        
        chapters_to_scrape = list(chap_dict.items())
        
        for chap_name, chap_href in tqdm(chapters_to_scrape, desc="下载章节"):
            if self.chapters_parsed_count >= max_chapters:
                self.logger.info(f"已达到 {max_chapters} 章节的下载限制。停止下载。")
                break

            chapter_html = self._fetch_html_content(chap_href)
            if chapter_html:
                content = self._parse_chapter_content(chapter_html)
                self.novel_data["content"].append({chap_name: content})
                self.chapters_parsed_count += 1
                self.logger.info(f"成功下载章节：'{chap_name}'")
            else:
                self.logger.warning(f"由于获取失败，跳过章节 '{chap_name}'。")
        return True

    def save_to_json(self, filename):
        """
        将爬取到的小说数据保存到 JSON 文件。
        :param filename: 保存的文件名。
        """
        try:
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(self.novel_data, f, ensure_ascii=False, indent=4)
            self.logger.info(f"小说数据已成功保存到 {filename}")
        except IOError as e:
            self.logger.error(f"保存文件 {filename} 失败: {e}")

# --- 使用示例 ---
if __name__ == '__main__':
    
    output_filename = "寒霜千年_oop.json"
    
    # 请根据实际网站的结构调整 base_site_url
    base_site_url = "https://www.c2186a.sbs/"
    chapter_list_url = base_site_url + "book/260609/"

    # 创建爬取器实例
    scraper = NovelScraper(base_url=base_site_url)

    # 开始爬取小说，这里设置为下载前 5 个章节
    success = scraper.scrape_novel(chap_url=chapter_list_url, max_chapters=5)

    # 如果爬取成功，则保存数据
    if success:
        scraper.save_to_json(output_filename)